#!/usr/bin/env python
"""
./execute.py [arguments] file.csp
Part of the CSPBuilder application.

Copyright (c) 2008 Rune M. Friborg, runef@diku.dk.
See LICENSE.txt for licensing details (GPLv2).

Usage:
  -i, --info               Print info on processes, channels
  -ii, --iinfo             Print .csp filenames
  -p, --profiling          Prints execution time
"""

import sys # to get cmdline arguments from sys.argv

# Local modules
from extensions.containers import * # Definitions
from extensions import cspxml
from extensions import stats
from plugins.execute import *

# External modules
import os # to do file / dir operations
from common import * # To update path

# Create and load plugin system
pluginman = PluginManagerEXECUTION()
pluginman.load()


def print_doc():
    print __doc__ + pluginman.getCommandLineHelp()


#Handle commandline arguments
OUTPUT_PARSED_CSPFILES = False
OUTPUT_INFO = False
OUTPUT_PROFILING = False
FILENAME = ''
    
if (len(sys.argv) > 1):
    args = sys.argv[1:]
    if ('--help' in args):
        print_doc()
        sys.exit(0)

    args = pluginman.parseCommandLineArgs(args)

    for arg in args[:]:
        if ('-i' == arg or '--info' == arg):
            OUTPUT_INFO = True
            del args[args.index(arg)]

        if ('-p' == arg or '--profiling' == arg):
            OUTPUT_PROFILING = True
            del args[args.index(arg)]

        if ('-ii' == arg or '--iinfo' == arg):
            OUTPUT_PARSED_CSPFILES = True
            del args[args.index(arg)]

         
    if (len(args) == 1):
        FILENAME = args[0]
    else:
        print_doc()
        sys.exit(0)
   
else:
    print_doc()
    sys.exit(0)





# Parse XML recursively
def parseXMLProcess(process):
    
    p = cspxml.getAttrList(process)    

    CODE = None
    PROCESSES = []
    PROCESSLINKS = []
    CPOINTS = []
    CHANNELS = []
    CHANNELENDS = []

    # Main processing
    for elem in process.childNodes:
        if (elem.nodeName == 'processlink'):
            att = cspxml.getAttrList(elem)

            # Find location and parse file.
            plist_document = None
            for path in pluginman.getList(pathlist_COMPONENTS) + [sys.path[0]]:
                pathAndFilename = path + '/' + att['file']
                if os.path.exists(pathAndFilename):
                    if (OUTPUT_PARSED_CSPFILES):
                        print pathAndFilename
                        
                    # Parse
                    plist_document = cspxml.parseFile(pathAndFilename)
                    break
            if not plist_document:
                print 'Could not locate file ' + att['file']
                sys.exit(1)

            for plist_e1 in plist_document.childNodes:
                if (plist_e1.nodeName == "doc"):
                    for plist_e2 in plist_e1.childNodes:
                        if (plist_e2.nodeName == "process"):
                            att2 = cspxml.getAttrList(plist_e2)
                            if (att2['name'] == att['process']):
                                # Interpret process
                                p2 = parseXMLProcess(plist_e2)

                                # Replace processname with processlink name
                                p2['name'] = att['name']

                                att['PROCESS'] = p2
                                
            att = pluginman.loadTags(node_PROCESS_LINK, elem, att)
            PROCESSLINKS.append(att)

        elif (elem.nodeName == 'channelend'):
            att = cspxml.getAttrList(elem)
            att = pluginman.loadTags(node_CHANNELEND, elem, att)
            CHANNELENDS.append(att)

        elif (elem.nodeName == 'process'):
            att = parseXMLProcess(elem)
            att = pluginman.loadTags(node_PROCESS, elem, att)
            PROCESSES.append(att)
            
        elif (elem.nodeName == 'cpoint'):
            att = cspxml.getAttrList(elem)
            att = pluginman.loadTags(node_CPOINT, elem, att)
            CPOINTS.append(att)

        elif (elem.nodeName == 'channel'):
            att = cspxml.getAttrList(elem)
            for con in elem.childNodes:
                if (con.nodeName == 'connection'):
                        att2 = cspxml.getAttrList(con)
                        if (att2['direction'] == 'in'):
                            att['inTarget'] = att2['target']
                        else:
                            att['outTarget'] = att2['target']
            att = pluginman.loadTags(node_CHANNEL, elem, att)
            CHANNELS.append(att)
            
        elif (elem.nodeName == 'code'):
            att = cspxml.getAttrList(elem)
            att = pluginman.loadTags(node_CODE, elem, att)
            CODE= att

    if PROCESSLINKS:
        p['PROCESSLINKS'] = PROCESSLINKS

    if PROCESSES:
        p['PROCESSES'] = PROCESSES

    if CHANNELENDS:
        p['CHANNELENDS'] = CHANNELENDS
    
    if CPOINTS:
        p['CPOINTS'] = CPOINTS
    
    if CHANNELS:
        p['CHANNELS'] = CHANNELS
    
    if CODE:
        p['CODE'] = CODE

    return p


# Convert Tree structure to flat structure (recursive)
def flattenProcessTree(p, path='', channelObjList = [], processObjList = []):
   
    stats.addTo('Processes', 1)

    process_name = path + '.' + p['name']

    codeObj = None
    channelEndObjList = []


    # Main processing
    if p.has_key('PROCESSLINKS'):
        for each in p['PROCESSLINKS']:
            flattenProcessTree(each['PROCESS'], process_name, channelObjList, processObjList)

    if p.has_key('PROCESSES'):
        for each in p['PROCESSES']:
            flattenProcessTree(each, process_name, channelObjList, processObjList)

    if p.has_key('CHANNELENDS'):
        for chanEnd in p['CHANNELENDS']:
            # Add to channelLookup
            #   - used to lookup channelends on process
            #   - used to multiply connections to channelends when multiplier is used.
            channelLookup[process_name + '.' + chanEnd['name']] = (process_name + '.' + chanEnd['target'], p['name'], chanEnd['name'])

            # If a code-element exists at same level, it will need this information
            channelEndObjList.append(ChannelEndDef(chanEnd['direction'], chanEnd['name']))


    # Create codeObj if exists
    if p.has_key('CODE'):
        c = p['CODE']

        # Find file
        filename = c['src']

        # Setting up execution 
        try:
            funcName = c['func']
            funcObj = __import__(filename).__dict__[funcName]
        except ImportError, value:
            print 'Error: Code could not be loaded:', filename, 'at', os.getcwd()
            print 'Error:', value
            return None                

        codeObj = CodeDef(funcName, funcObj, channelEndObjList)        



    # Extract data structure
    cpointObjList = []
    channelLineObjList = []
    inTarget = ''
    outTarget = ''

    if p.has_key('CPOINTS'):
        for cpoint in p['CPOINTS']:
            cpointObjList.append(CPointDef(cpoint['name'], cpoint))
            
    if p.has_key('CHANNELS'):
        for channel in p['CHANNELS']:
            channelLineObjList.append(ChannelLineDef(channel['name'], channel['inTarget'], channel['outTarget'], channel))


    # Reduce cpoints
    for cpoint in cpointObjList:
        connectedCount = 0
        connections = []
        for channel in channelLineObjList:
            if channel.inTarget == cpoint.name + '.' and channel.outTarget != cpoint.name:
                connectedCount = connectedCount + 1
                connections.append(channel.outTarget)
            elif channel.outTarget == cpoint.name + '.' and channel.inTarget != cpoint.name:
                connectedCount = connectedCount + 1
                connections.append(channel.inTarget)

        if (connectedCount == 1):
            # Remove cpoint and all channels connected to it
            cpoint.REMOVE = True
        else:
            # Search for first cpoint connected to this cpoint
            found = None
            for connection in connections:
                for cpoint_search in cpointObjList:
                    if (not cpoint_search.REMOVE and
                        cpoint_search.name != cpoint.name and
                        cpoint_search.name + '.' == connection):
                        found = cpoint_search
                        break
                if (found):
                    break

            # Redirect channelends from cpoint to found
            if (found):                
                for channel in channelLineObjList:
                    # Redirect!
                    if channel.inTarget == cpoint.name + '.':
                        channel.inTarget = found.name + '.'
                    if channel.outTarget == cpoint.name + '.':
                        channel.outTarget = found.name + '.'

                # Pass on any plugin attributes
                pluginman.preserveAttributes(found.plugin_attr, cpoint.plugin_attr)

                cpoint.REMOVE = True

    # Remove channels with identical inTarget and outTarget
    for channel in channelLineObjList:
        if (channel.inTarget == channel.outTarget):
            channel.REMOVE = True

    # Reducing cpoints and channels into new channels
    reducedChannelObjList = []

    # Create reducedChannels structure
    for cpoint in cpointObjList:
        if (not cpoint.REMOVE):
            connectionObjList = []
            for channel in channelLineObjList:
                if (not channel.REMOVE):
                    if (channel.inTarget == cpoint.name + '.'):
                        channel.cpoint_conn = True
                        connectionObjList.append(ConnectionDef(channel.outTarget, 'out'))
                    elif (channel.outTarget == cpoint.name + '.'):
                        channel.cpoint_conn = True
                        connectionObjList.append(ConnectionDef(channel.inTarget, 'in'))
            reducedChannel = ReducedChannelDef('chan_' + cpoint.name, cpoint.plugin_attr)
            reducedChannel.connectionObjList = connectionObjList
            reducedChannelObjList.append(reducedChannel)


    # Add One2One channels, not connected with any cpoints
    i = 0
    for channel in channelLineObjList:
        if not (channel.REMOVE or channel.cpoint_conn):
            connectionObjList = []
            connectionObjList.append(ConnectionDef(channel.outTarget, 'out'))
            connectionObjList.append(ConnectionDef(channel.inTarget, 'in'))
            reducedChannel = ReducedChannelDef(channel.name, channel.plugin_attr)
               
            reducedChannel.connectionObjList = connectionObjList
            reducedChannelObjList.append(reducedChannel)

    # Do channels after other structures, because process structures are needed.
    for channel in reducedChannelObjList:
        channel.addAndLookupPaths(channelLookup, path = process_name)
        channelObjList.append(channel)

    # If code exist for this process, then create as CSP process
    if codeObj:
        processObj = ProcessDef(process_name, codeObj)
        processObjList.append(processObj)

    return (channelObjList, processObjList)


def updateConnections(processObjList):

    # Find correct name + path for each channelend, and update each process with the list of channelends.
    for process in processObjList:
        L = []
        for end in process.codeObj.channelEndObjList:
            L.append(process.name + '.' + process.codeObj.funcName + '.' + end.name)
        process.finalChannelEndList = L


def buildNetwork(channelObjList, processObjList):

    # Create channel objects
    for channel in channelObjList:
        stats.addTo('Channels', 1)
        channel.instance = Channel(channel.name)

    unConnectedChannels = []
    cspProcessList = []

    for process in processObjList:

        paramList = [process.codeObj.funcObj]
        paramInList = []
        paramOutList = []
        for channelend in process.finalChannelEndList:
            #Find Channel
            found = False
            for channel in channelObjList:
                for connection in channel.connectionObjList:
                    if (channelend == connection.target):
                        found = True

                        # Connect!
                        if (connection.direction == 'in'):
                           paramInList.append(IN(channel.instance))
                        elif (connection.direction == 'out'):
                           paramOutList.append(OUT(channel.instance))

            if (not found):
                unConnectedChannels.append(channelend)

        # Add plugin functionalities on process creation
        (paramList, paramDict) = pluginman.editProcessCreation(process, paramList, {})

        cspProcessList.append(apply ( Process, paramList + paramInList + paramOutList, paramDict) )


    stats.addTo('CSP Processes', len(cspProcessList))
    stats.profileEnd('Setup CSP Network')
    stats.profileStart('Executing CSP Network')


    # Output statistics
    if (OUTPUT_INFO):
        stats.setOrder('CSP Processes', 1)
        #stats.setOrder('Processes', 1)
        stats.setOrder('Channels', 2)
        print stats.getFormattedCounts()

    sys.stdout.flush()

    if (len(unConnectedChannels)):
        print 'Error: Unconnected channels:'
        for x in unConnectedChannels:
            print "\t",x
        return False
    else:
        # Shell to interact with execution
        apply ( Parallel, cspProcessList )
    return True


# Main
sys.path = pluginman.updatePath(sys.path)

stats.profileStart('Everything')
stats.profileStart('Parse CSP XML')

# Used for unfolding the entire path to a channelconnection
channelLookup = {}

# Parse application csp file
document = cspxml.parseFile(FILENAME)

# Extract root process node
root = {}
for e1 in document.childNodes:
    if (e1.nodeName == "doc"):
        for e2 in e1.childNodes:
            if (e2.nodeName == "process"):
                root = parseXMLProcess(e2)

# Run plugins
root = pluginman.editProcessTreePlugin(root)

# Remove topology from CSP network
(channelObjList, processObjList) = flattenProcessTree(root, 'main')

# Run plugins
(channelObjList, processObjList) = pluginman.editReducedSet(channelObjList, processObjList)

# Find all matching channel ends <-> processes
updateConnections(processObjList)


stats.profileEnd('Parse CSP XML')
stats.profileStart('Setup CSP Network')

execVal = buildNetwork(channelObjList, processObjList)
if (execVal):
    stats.profileEnd('Executing CSP Network')
    stats.profileEnd('Everything')

    # Output statistics
    if (OUTPUT_PROFILING):
        # Update profiling
        print ''
        print stats.getFormattedProfiling()

else:
    print 'Failed execution.'
   
document.unlink()
